/**
 
 * 
 */
package nl.boektrust.btsoep.server;

import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import nl.boektrust.btsoep.shared.model.AvailabilityCode;
import nl.boektrust.btsoep.shared.model.BTEntity;
import nl.boektrust.btsoep.shared.model.Book;
import nl.boektrust.btsoep.shared.model.BookManifestation;
import nl.boektrust.btsoep.shared.model.ContentSpecifier;
import nl.boektrust.btsoep.shared.model.ContentSpecifierType;
import nl.boektrust.btsoep.shared.model.Contributor;
import nl.boektrust.btsoep.shared.model.ContributorRoleCode;
import nl.boektrust.btsoep.shared.model.ImageReferenceTypeCode;
import nl.boektrust.btsoep.shared.model.LanguageCode;
import nl.boektrust.btsoep.shared.model.Person;
import nl.boektrust.btsoep.shared.model.PhysicalBookManifestation;
import nl.boektrust.btsoep.shared.model.Price;
import nl.boektrust.btsoep.shared.model.PriceTypeCode;
import nl.boektrust.btsoep.shared.model.ProductFormCode;
import nl.boektrust.btsoep.shared.model.Supplier;
import nl.boektrust.btsoep.shared.model.SupplyDetail;
import nl.boektrust.btsoep.shared.model.TextType;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyService;

/**
 * @author rao3
 *
 * the implementation of the server that can serve XML requests to Koppelvlak bookstores. 
 */
public class ImportCBOnixTask extends HttpServlet {
	/**
	 * 
	 */
	
	private static final long serialVersionUID = 1L;
	private boolean isObjectifyServiceRegistered = false;
	private ServletContext context;
	String logMessage = new String("");
	int skippedBooks = 0;
	int deletedBooks = 0;
	private PersistentServiceImpl persistentServiceImpl =new PersistentServiceImpl();

	
	ServletContext getContext() {
		return this.context;
	}

	public void init() throws ServletException {
		context = getServletContext();
		logMessage = new String("");
		persistentServiceImpl.registerServices();
		//System.err.println("Real Path = " + context.getRealPath("/"));
	}

	/**
	 * 
	 */
	public ImportCBOnixTask() {
	}

	/**
	 * @param delegate
	 */
	public ImportCBOnixTask(Object delegate) {
		//super(delegate);
	}
	
	
	
	public void sendResponse(javax.servlet.http.HttpServletResponse response, String responseString) {
		response.setStatus(200);
		try {
			/*
			 * send with XMPP using the sendmessage servlet on the frontend
			 */
			URL messagingurl = new URL("http://btsoep.appspot.com/sendmessage?message="+URLEncoder.encode(responseString, "ascii"));
			BufferedReader reader = new BufferedReader(new InputStreamReader(messagingurl.openStream()));
			reader.close();

			/*
			 * send same response to the browser
			 */
			response.getOutputStream().print("ImportCBOnixTask: "+ responseString+"\n");
			response.getOutputStream().close();

		} catch (IOException e) {
			System.err.println("ImportCBONixTask: IO exception thrown while sending HTTP response");
			System.err.println(stack2string(e));

		} catch (Exception e) {
			System.err.println("ImportCBONixTask: General exception thrown while sending HTTP response");
		}
	}
	
	public void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws ServletException, IOException  
	{
		
		/*
		 * get raw XML from the inputstream. 
		 */
		System.err.println("");
		System.err.println("TASK: read and parse CB record. Parameter URL is: "+request.getParameter("url"));
		String urlString = request.getParameter("url");
		ArrayList<PhysicalBookManifestation> list = null;
		skippedBooks = 0;
		deletedBooks = 0;
		

		/*
		 * check if parameter URL is valid 
		 */
		if (urlString == null || urlString.length() == 0) {
			System.err.println("readCB was called with no argument, expected a filename");
			response.sendError(400, "url was not specified");
			return;
		}

		/* 
		 * read in and parse the ONIX record 
		 */
		int numProductNodes;
		try {
			list = readCB(urlString);
		} catch (IOException e) {
			sendResponse(response, "IOException while reading URL "+urlString+": "+e.getMessage());
			return;
		}
		if (list == null) {
			sendResponse(response, "Serious error: readCB for this URL returned null");
			return;
		}
		
		/*
		 * write out the found records into the database
		 */
		for (PhysicalBookManifestation bb : list) {
			System.out.println("processing recref "+bb.getRecordReference());
			if (!(new Long(bb.getRecordReference()).toString().startsWith("978906262"))) {
				System.out.println("  passed boektrust test");
				if (new Long(bb.getRecordReference()).toString().length() > 10) {
					System.out.println("  passed length test");
					/*
					 * check if newer version exist; if no, delete prior versions if necessary and add book.
					 */
					
					List<Key<PhysicalBookManifestation>> newerBookKeys = persistentServiceImpl.getBookKeysByRecordReferenceNewerThan(bb.getRecordReference(), bb.getFirstModificationDate());
					if (newerBookKeys.size() >= 0) {
						List<Key<PhysicalBookManifestation>> olderBookKeys = persistentServiceImpl.getBookKeysByRecordReferenceOlderThan(bb.getRecordReference(), bb.getFirstModificationDate());
						System.err.println("Removing prior "+olderBookKeys.size()+ " books for recref "+bb.getRecordReference());
						for (Key<PhysicalBookManifestation> bookToDelete : olderBookKeys) {
							persistentServiceImpl.deletePhysicalBookManifestation(bookToDelete);
							deletedBooks++;
						}
						persistentServiceImpl.persistPhysicalBookManifestation(bb);
					}
				}
			}
		}
		int newBooks = -1;
		int updatedBooks = -1;
		sendResponse(response, "ImportCBOnixTask finished importing URL "+urlString+"; added "+newBooks+", updated "+updatedBooks+", skipped "+skippedBooks+", deleted "+deletedBooks+"\n");
	}
	
	

	
	public void doGet(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {
		this.doPost(request, response);	
	}
	
	/*
	 * function to parse a productnode as sent in CB Onix mutations
	 */
	public PhysicalBookManifestation parseProductNode(Node n, Date d) {
		PhysicalBookManifestation b = new PhysicalBookManifestation();
		b.setFirstModificationDate(d);
		NodeList nl = n.getChildNodes();
		Supplier supplier = new Supplier();
		supplier.setName("Centraal Boekhuis");
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals("RecordReference")) {
				//System.err.println("Setting record reference: "+textContent);
				b.setRecordReference(Long.parseLong(textContent));
				//System.err.println("Set  record reference to: "+b.getRecordReference());
				try {
					b.setIsbn(nl.item(i).getFirstChild().getTextContent());
				} catch (Exception e) {
					e.printStackTrace();
				}
			} else if (nl.item(i).getNodeName().equals("ProductForm")) {
				b.setProductFormCode(ProductFormCode.parseOnixStringIntoProductFormCode(textContent));
			} else if (nl.item(i).getNodeName().equals("ProductClassification")) {
				/* this is a CB specific field, ignored */
			} else if (nl.item(i).getNodeName().equals("Title")) {
				if (cbParseTitleType(nl.item(i)) == 1) {
					b.setTitle(cbParseTitleText(nl.item(i)));
				}
				if (cbParseTitleSubtitle(nl.item(i)) != null && cbParseTitleSubtitle(nl.item(i)).length() > 0) {
					b.setSubtitle(cbParseTitleSubtitle(nl.item(i)));
				}
			} else if (nl.item(i).getNodeName().equals("Contributor")) {
				Contributor c = new Contributor();
				c.setNameOnBook(cbParseContributorName(nl.item(i)));
				c.setRoleCode(cbParseContributorRoleCode(nl.item(i)));
				Person p = persistentServiceImpl.getPersonByFullName(c.getNameOnBook());

				if (p == null) {
					p = new Person();
					p.setName(c.getNameOnBook());
				}
				c.setPerson(p);
				b.addContributor(c);
			} else if (nl.item(i).getNodeName().equals("EditionNumber")) {
				b.setEdition(Integer.parseInt(textContent));
			} else if (nl.item(i).getNodeName().equals("Language")) {
				if (cbParseNumField(nl.item(i), "LanguageRole") == 1) {
					b.setLanguageCode(cbParseLanguageCode(nl.item(i)));
				}
			} else if (nl.item(i).getNodeName().equals("PagesArabic")) {
				b.setNumberOfPages(Integer.parseInt(textContent));
			} else if (nl.item(i).getNodeName().equals("Illustrations")) {
				
				
			} else if (nl.item(i).getNodeName().equals("MainSubject")) {
				if (cbParseNumField(nl.item(i), "MainSubjectSchemeIdentifier") == 32) {
					ContentSpecifier cs = new ContentSpecifier();
					cs.setContentSpecifierType(ContentSpecifierType.NURCODE);
					cs.setCode(cbParseNumField(nl.item(i), "SubjectCode"));
					b.addContentSpecifier(cs);
				}
			} else if (nl.item(i).getNodeName().equals("OtherText")) {
				if (cbParseNumField(nl.item(i), "TextTypeCode") == 18) {
					nl.boektrust.btsoep.shared.model.Text t = new nl.boektrust.btsoep.shared.model.Text();
					t.setText(cbParseStringField(nl.item(i), "Text"));
					t.setTextType(TextType.COVERTEXT);
				}
			} else if (nl.item(i).getNodeName().equals("Imprint")) {
				b.setImprint(textContent);
			} else if (nl.item(i).getNodeName().equals("MediaFile")) {
				String urlString = cbParseStringField(nl.item(i), "MediaFileLink");
				ImageReferenceTypeCode imageTypeCode = ImageReferenceTypeCode.UNDEFINED;
				if (cbParseNumField(nl.item(i), "MediaFileTypeCode") == 4) {
					imageTypeCode = ImageReferenceTypeCode.FRONTCOVER;
				} else if (cbParseNumField(nl.item(i), "MediaFileTypeCode") == 25) {
					imageTypeCode = ImageReferenceTypeCode.BACKCOVER;
				} 
				//downloadAsset(b.getRecordReference(), urlString, imageTypeCode);
			} else if (nl.item(i).getNodeName().equals("Publisher")) {
			} else if (nl.item(i).getNodeName().equals("PublishingStatus")) {
			} else if (nl.item(i).getNodeName().equals("PublicationDate")) {
				try {
				    // Some examples
				    DateFormat formatter = new SimpleDateFormat("yyyyMMdd");
				    Date date = (Date)formatter.parse(textContent);
				} catch (java.text.ParseException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} else if (nl.item(i).getNodeName().equals("Measure")) {
				int measureTypeCode = cbParseNumField(nl.item(i), "MeasureTypeCode");
				switch (measureTypeCode) {
				case 1: 
					b.setHeight(cbParseNumField(nl.item(i), "Measurement")); 
					break;
				case 2 : 
					b.setWidth(cbParseNumField(nl.item(i), "Measurement")); 
					break;
				case 3 : 
					b.setThickness(cbParseNumField(nl.item(i), "Measurement")); 
					break;
				case 8 : 
					b.setWeight(cbParseNumField(nl.item(i), "Measurement")); 
					break;
				}
			} else if (nl.item(i).getNodeName().equals("SupplyDetail")) {
				/*
				 * extract price node
				 */
				SupplyDetail s = new SupplyDetail();
				NodeList priceNl = nl.item(i).getChildNodes();
				for (int j=0; j<priceNl.getLength(); j++) { 
					String nodeName2 = priceNl.item(j).getNodeName();
					if (nodeName2.equals("Price")) {
						if (cbParseNumField(priceNl.item(j), "PriceTypeCode") == 2) {
							Price p = new Price();
							p.setPriceTypeCode(PriceTypeCode.MSRP);
							//System.err.println("prijs");
							float prijs = cbParseFloatField(priceNl.item(j), "PriceAmount")*100;
							p.setPriceAmountInCents((int)prijs);
							//System.err.println("Prijs stored: "+p.getPriceAmountInCents());
							s.addPrice(p);
						}
					} else if (nodeName2.equals("ProductAvailability")) {
						//System.err.println("parseProductNode: parsing ProductAvailability code " + priceNl.item(j).getFirstChild().getTextContent());

						s.setAvailabilityCode(AvailabilityCode.parseIntIntoAvailabilityCode(Integer.parseInt(priceNl.item(j).getFirstChild().getTextContent())));

					}
				}
				s.setSupplier(supplier);
				b.addSupplyDetail(s);
			}
		}
		return b;
	}
	
	public int cbParseNumField(Node n, String fieldname) {
		int type = -1;
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			if (nl.item(i) == null) return -1;
			if (nl.item(i).getFirstChild() == null) return -1;
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals(fieldname)) {
				try {
					if (fieldname.equals("PriceAmount") == true) 
						System.err.println("Price in XML is: " + textContent);

					type = (int) Float.parseFloat(textContent);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return type;
	}
	
	public float cbParseFloatField(Node n, String fieldname) {
		float type = -1;
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			if (nl.item(i) == null) return -1;
			if (nl.item(i).getFirstChild() == null) return -1;
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals(fieldname)) {
				try {
					type = Float.parseFloat(textContent);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return type;
	}
	
	public String cbParseStringField(Node n, String fieldname) {
		String string = new String("");
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals(fieldname)) {
				string = textContent;
			}
		}
		return string;
	}
	
	public int cbParseTitleType(Node n) {
		int type = -1;
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals("TitleType")) {
				try {
					type = Integer.parseInt(textContent);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return type;
	}
	
	public String cbParseTitleText(Node n) {
		String title = new String("");
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals("TitleText")) {
				title = textContent;
			}
		}
		return title;
	}
	
	public String cbParseTitleSubtitle(Node n) {
		String title = new String("");
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals("Subtitle")) {
				title = textContent;
			}
		}
		return title;
	}
	
	public ContributorRoleCode cbParseContributorRoleCode(Node n) {
		ContributorRoleCode code = ContributorRoleCode.UNDEFINED;
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals("ContributorRole")) {
				//System.err.println("Content  : "+textContent);
				code = ContributorRoleCode.parseOnixStringIntoContributorRoleCode(textContent);
				//System.err.println("Parsed as: "+ContributorRoleCode.stringValue(code));

			}
		}
		return code;
	}
	
	public String cbParseContributorName(Node n) {
		String name = new String("");
		HashMap<String,String> namesSet = new HashMap<String,String>();
		ArrayList<String> keys = new ArrayList<String>();
		keys.add("TitlesBeforeNames");
		keys.add("NamesBeforeKey");
		keys.add("PrefixToKey");
		keys.add("KeyNames");
		keys.add("NamesAfterKey");
		keys.add("SuffixToKey");
		keys.add("LettersAfterNames");
		keys.add("TitlesAfterNames");

		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			for (String key : keys) {
				if (nodeName.equals(key)) { 
					namesSet.put(nodeName, textContent);
				}
			}
		}
		for (String key : keys) {
			if (namesSet.containsKey(key)) {
				if (name.length() > 0) name = name+" ";
				name = name+namesSet.get(key);
			}
		}
		return name;
	}

	public LanguageCode cbParseLanguageCode(Node n) {
		LanguageCode code = LanguageCode.UNDEFINED;
		NodeList nl = n.getChildNodes();
		for (int i=0; i<nl.getLength(); i++) { 
			String textContent = nl.item(i).getFirstChild().getTextContent();
			String nodeName = nl.item(i).getNodeName();
			if (nodeName.equals("LanguageCode")) {
				code = LanguageCode.parseOnixStringIntoLanguageCode(textContent);

			}
		}
		return code;
	}
	
	/* 
	 * this function reads in a CB record from the URL given, returns number of product nodes read. 
	 */
	public ArrayList<PhysicalBookManifestation> readCB(String filename) throws IOException {
		ArrayList<PhysicalBookManifestation> list = new ArrayList<PhysicalBookManifestation>();
		String dateString = new String("");
		int numProductNodes = 0;
		
		/*
		 * read the ONIX message in as a raw string from the URL that was passed in as parameter.
		 */
		URL url = new URL(filename);
		BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
		String line = new String("");
		String buffer = new String("");

		while ((line = reader.readLine()) != null) {
			//System.err.println(line);
			buffer = buffer + line;
		}
		reader.close();
		logMessage = logMessage + "downloaded CB update: "+buffer.length()+" bytes\n";

		/* 
		 * parse the ONIX Message into product chunks 
		 */
		try {
			javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
			javax.xml.parsers.DocumentBuilder db = factory.newDocumentBuilder();
			InputSource inStream = new org.xml.sax.InputSource();

			inStream.setCharacterStream(new java.io.StringReader(buffer));

			Document doc = db.parse(inStream);

			NodeList nl = doc.getDocumentElement().getChildNodes();
			//if (nl != null) System.err.println("Documentnode has "+nl.getLength()+" children");
			if (doc.getDocumentElement().getNodeName().equals("ONIXMessage")) {
				Date msgDate = new Date();

				nl = doc.getDocumentElement().getChildNodes();
				numProductNodes = nl.getLength();
				logMessage = logMessage + "This ONIX message has "+numProductNodes+" nodes\n";
				for (int i=0; i<nl.getLength(); i++) {
					if (nl.item(i).getNodeName().equals("Header")) {
						NodeList headernl = nl.item(i).getChildNodes();
						dateString = cbParseStringField(nl.item(i), "SentDate");
						try {
							DateFormat formatter = new SimpleDateFormat("yyyyMMddhhmm");
							msgDate = (Date)formatter.parse(dateString);
						} catch (java.text.ParseException e) {
							e.printStackTrace();
						}

					} else if (nl.item(i).getNodeName().equals("Product")) {
						PhysicalBookManifestation b = parseProductNode(nl.item(i), msgDate);
						list.add(b);
					}
				}
			}

		}  catch (Exception e) {
			System.err.println(stack2string(e));
		}
		System.err.println(logMessage);
		return list;
	}

	/*
	 * 						PhysicalBookManifestation b_existing = persistentServiceImpl.getBookByRecordReference(b.getRecordReference());
						if (b_existing == null || b_existing.getFirstModificationDate().compareTo(b.getFirstModificationDate()) <= 0 ) {
							if (b_existing == null) {
								logMessage = logMessage + "CB update "+dateString+": new book " + b.getRecordReference() + "\n";
								booksToAddList.add(b);
							} else if (b_existing.getFirstModificationDate().compareTo(b.getFirstModificationDate()) <= 0) {
								logMessage = logMessage + "CB update "+dateString+": updating book " + b.getRecordReference() + "\n";
								booksToUpdateList.add(b);    	    				

							}
						} else {
							logMessage = logMessage + "CB update "+dateString+": skipping (preexisting/newer) book " + b.getRecordReference() + "\n";
							skippedBooks++;
						}

						//	    			System.err.println(b.toPrintString());

	 */
	public static org.w3c.dom.DocumentFragment getDocumentFragment() {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			try {
				DocumentBuilder db = factory.newDocumentBuilder();
				org.w3c.dom.Document doc = db.newDocument();
				org.w3c.dom.DocumentFragment documentFragment = doc.createDocumentFragment();
				return documentFragment;

			} catch (Exception e) {
				e.printStackTrace();
			}

			return null;
			
		}


	public static String stack2string(Exception e) {
		try {
			StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			e.printStackTrace(pw);
			return "------\r\n" + sw.toString() + "------\r\n";
		}
		catch(Exception e2) {
			return "bad stack2string";
		}
	}


	private void downloadAsset(long recordReference, String urlString, ImageReferenceTypeCode imageCode) {
		Queue queue = QueueFactory.getQueue("images");
		System.out.println("Adding download task to queue for rec ref "+recordReference+" imageCode "+imageCode);
		queue.add(withUrl("/importimagefiletask").param("urlString", urlString).param("recordReference", new Long(recordReference).toString()).param("imageType", imageCode.toString()));
		
	}
}
